#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using ShopifySharp.Entities;
using ShopifySharp.Enums;
using ShopifySharp.Infrastructure;
using ShopifySharp.Infrastructure.Serialization.Json;

namespace ShopifySharp.Utilities;

public interface IShopifyOauthUtility
{
    /// <summary>
    /// Builds an OAuth authorization URL for Shopify OAuth integration.
    /// </summary>
    /// <param name="scopes">An array of <see cref="AuthorizationScope"/> — the permissions that your app needs to run.</param>
    /// <param name="shopDomain">The shop's *.myshopify.com URL.</param>
    /// <param name="clientId">Your app's public Client ID, also known as its public API key.</param>
    /// <param name="redirectUrl">URL to redirect the user to after integration.</param>
    /// <param name="state">An optional, random string value provided by your application which is unique for each authorization request. During the OAuth callback phase, your application should check that this value matches the one you provided to this method.</param>
    /// <param name="grants">Requested grant types, which will change the type of access token granted upon OAuth completion.</param>
    /// <remarks>
    /// Use the <see cref="BuildAuthorizationUrl(AuthorizationUrlOptions)"/> overload instead.
    /// </remarks>
    [Obsolete("Use " + nameof(BuildAuthorizationUrl) + "(" + nameof(AuthorizationUrlOptions) + ") instead. This method will be removed in a future version of ShopifySharp.")]
    Uri BuildAuthorizationUrl(
        IEnumerable<AuthorizationScope> scopes,
        string shopDomain,
        string clientId,
        string redirectUrl,
        string? state = null,
        IEnumerable<string>? grants = null
    );

    /// <summary>
    /// Builds an OAuth authorization URL for Shopify OAuth integration.
    /// </summary>
    /// <param name="scopes">An array of Shopify permission strings, e.g. 'read_orders' or 'write_script_tags'. These are the permissions that your app needs to run.</param>
    /// <param name="shopDomain">The shop's *.myshopify.com URL.</param>
    /// <param name="clientId">Your app's public Client ID, also known as its public API key.</param>
    /// <param name="redirectUrl">URL to redirect the user to after integration.</param>
    /// <param name="state">An optional, random string value provided by your application which is unique for each authorization request. During the OAuth callback phase, your application should check that this value matches the one you provided to this method.</param>
    /// <param name="grants">Requested grant types, which will change the type of access token granted upon OAuth completion.</param>
    [Obsolete("Use " + nameof(BuildAuthorizationUrl) + "(" + nameof(AuthorizationUrlOptions) + ") instead. This method will be removed in a future version of ShopifySharp.")]
    Uri BuildAuthorizationUrl(
        IEnumerable<string> scopes,
        string shopDomain,
        string clientId,
        string redirectUrl,
        string? state = null,
        IEnumerable<string>? grants = null
    );

    /// <summary>
    /// Builds an OAuth authorization URL for Shopify OAuth integration.
    /// </summary>
    /// <param name="options">Options for building the OAuth URL.</param>
    Uri BuildAuthorizationUrl(AuthorizationUrlOptions options);

    /// <summary>
    /// Authorizes an application installation, generating an access token for the given shop.
    /// </summary>
    /// <param name="code">The authorization code generated by Shopify, which is attached to the redirect querystring when Shopify redirects the user back to your app.</param>
    /// <param name="shopDomain">The store's *.myshopify.com URL, which is attached as a parameter named <c>shop</c> on the redirect querystring.</param>
    /// <param name="clientId">Your app's public Client ID, also known as its public API key.</param>
    /// <param name="clientSecret">Your app's Client Secret, also known as its secret API key.</param>
    Task<AuthorizationResult> AuthorizeAsync(
        string code,
        string shopDomain,
        string clientId,
        string clientSecret
    );

    /// <summary>
    /// Authorizes an application installation, generating an access token for the given shop.
    /// </summary>
    /// <param name="options">Options for performing the authorization.</param>
    Task<AuthorizationResult> AuthorizeAsync(AuthorizeOptions options);

    /// <summary>
    /// Refreshes an existing store access token using the app's client secret and a refresh token
    /// For more info on rotating tokens, see https://shopify.dev/apps/auth/oauth/rotate-revoke-client-credentials
    /// </summary>
    /// <param name="shopDomain">The store's *.myshopify.com url</param>
    /// <param name="clientId">Your app's public Client ID, also known as its public API key.</param>
    /// <param name="clientSecret">Your app's Client Secret, also known as its secret API key.</param>
    /// <param name="refreshToken">The app's refresh token</param>
    /// <param name="existingStoreAccessToken">The existing store access token</param>
    Task<AuthorizationResult> RefreshAccessTokenAsync(
        string shopDomain,
        string clientId,
        string clientSecret,
        string refreshToken,
        string existingStoreAccessToken
    );

    /// <summary>
    /// Refreshes an existing store access token using the app's client secret and a refresh token
    /// For more info on rotating tokens, see https://shopify.dev/apps/auth/oauth/rotate-revoke-client-credentials
    /// </summary>
    /// <param name="options">Options for refreshing the access token.</param>
    Task<AuthorizationResult> RefreshAccessTokenAsync(RefreshAccessTokenOptions options);
}

public class ShopifyOauthUtility: IShopifyOauthUtility
{
    private const string AccessTokenPropertyName = "access_token";
    private const string ExpiresInPropertyName = "expires_in";
    private const string AssociatedUserPropertyName = "associated_user";
    private const string AssociatedUserScopePropertyName = "associated_user_scope";
    private const string ScopePropertyName = "scope";

    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IShopifyDomainUtility _domainUtility;
    private readonly IJsonSerializer _jsonSerializer;

    public ShopifyOauthUtility(IShopifyDomainUtility? domainUtility = null)
    {
        (_domainUtility, _httpClientFactory, _jsonSerializer) = InitializeDependencies(null, domainUtility);
    }

    internal ShopifyOauthUtility(IServiceProvider serviceProvider)
    {
        (_domainUtility, _httpClientFactory, _jsonSerializer) = InitializeDependencies(serviceProvider, null);
    }

    private static (IShopifyDomainUtility, IHttpClientFactory, IJsonSerializer) InitializeDependencies(IServiceProvider? serviceProvider, IShopifyDomainUtility? shopifyDomainUtility)
    {
        var domainUtility = InternalServiceResolver.GetServiceOrDefault(
            serviceProvider, () => shopifyDomainUtility ?? new ShopifyDomainUtility());
        var httpClientFactory = InternalServiceResolver.GetServiceOrDefault<IHttpClientFactory>(
            serviceProvider, () => new InternalHttpClientFactory());
        var jsonSerializer = InternalServiceResolver.GetServiceOrDefault<IJsonSerializer>(
            serviceProvider, () => new SystemJsonSerializer(GetJsonSerializerOptions()));

        return (domainUtility, httpClientFactory, jsonSerializer);

        JsonSerializerOptions GetJsonSerializerOptions() => InternalServiceResolver.GetServiceOrDefault(
            serviceProvider, () => Serializer.RestSerializerOptions);
    }

    /// <inheritdoc />
    [Obsolete("Use " + nameof(BuildAuthorizationUrl) + "(" + nameof(AuthorizationUrlOptions) + ") instead. This method will be removed in a future version of ShopifySharp.")]
    public Uri BuildAuthorizationUrl(
        IEnumerable<AuthorizationScope> scopes,
        string shopDomain,
        string clientId,
        string redirectUrl,
        string? state = null,
        IEnumerable<string>? grants = null
    ) => BuildAuthorizationUrl(new AuthorizationUrlOptions
    {
        Scopes = scopes.Select(s => s.ToSerializedString()),
        ShopDomain = shopDomain,
        ClientId = clientId,
        RedirectUrl = redirectUrl,
        State = state,
#pragma warning disable CS0618 // Type or member is obsolete
        Grants = grants,
#pragma warning restore CS0618 // Type or member is obsolete
    });

    /// <inheritdoc />
    [Obsolete("Use " + nameof(BuildAuthorizationUrl) + "(" + nameof(AuthorizationUrlOptions) + ") instead. This method will be removed in a future version of ShopifySharp.")]
    public Uri BuildAuthorizationUrl(
        IEnumerable<string> scopes,
        string shopDomain,
        string clientId,
        string redirectUrl,
        string? state = null,
        IEnumerable<string>? grants = null
    ) => BuildAuthorizationUrl(new AuthorizationUrlOptions
    {
        Scopes = scopes,
        ShopDomain = shopDomain,
        ClientId = clientId,
        RedirectUrl = redirectUrl,
        State = state,
#pragma warning disable CS0618 // Type or member is obsolete
        Grants = grants,
#pragma warning restore CS0618 // Type or member is obsolete
    });

    /// <inheritdoc />
    public Uri BuildAuthorizationUrl(AuthorizationUrlOptions options)
    {
        var builder = new UriBuilder(_domainUtility.BuildShopDomainUri(options.ShopDomain));
        var qs = new List<(string, string)>
        {
            ("client_id", options.ClientId),
            ("scope", string.Join(",", options.Scopes)),
            ("redirect_uri", options.RedirectUrl),
        };

        if (!string.IsNullOrEmpty(options.State))
            qs.Add(("state", options.State!));

        if (options.AuthorizationAccessMode == AuthorizationAccessMode.Online)
        {
            // To use the online access mode, set the grant_options[] value to per-user
            qs.Add(("grant_options[]", "per-user"));

#pragma warning disable CS0618 // Type or member is obsolete
            if (options.Grants?.Any() == true)
                throw new ArgumentException($"Invalid {nameof(AuthorizationUrlOptions)}. Cannot use the obsolete {nameof(options.Grants)} alongside {nameof(options.AuthorizationAccessMode)}.");
        }
        else if (options.Grants?.ToList() is { Count: >= 1 } grants)
#pragma warning restore CS0618 // Type or member is obsolete
        {
            qs.AddRange(grants.Select(grant => ("grant_options[]", grant)));
        }

        builder.Path = "admin/oauth/authorize";
        builder.Query = string.Join("&", qs.Select(pairs => $"{pairs.Item1}={pairs.Item2}"));

        return builder.Uri;
    }

    /// <inheritdoc />
    public Task<AuthorizationResult> AuthorizeAsync(AuthorizeOptions options) =>
        AuthorizeAsync(
            options.Code,
            options.ShopDomain,
            options.ClientId,
            options.ClientSecret
        );

    /// <inheritdoc />
    public async Task<AuthorizationResult> AuthorizeAsync(
        string code,
        string shopDomain,
        string clientId,
        string clientSecret
    )
    {
        var ub = new UriBuilder(_domainUtility.BuildShopDomainUri(shopDomain))
        {
            Path = "admin/oauth/access_token"
        };
        using var content = new JsonContent(new { client_id = clientId, client_secret = clientSecret, code });
        using var request = new CloneableRequestMessage(ub.Uri, HttpMethod.Post, content);

        return await SendRequestAndParseAuthorizationResultAsync(request);
    }

    private async Task<AuthorizationResult> SendRequestAndParseAuthorizationResultAsync(CloneableRequestMessage requestMessage)
    {
        var client = _httpClientFactory.CreateClient(nameof(ShopifyOauthUtility));
        using var response = await client.SendAsync(requestMessage, CancellationToken.None);
        var json = await response.Content.ReadAsStringAsync();

        ShopifyService.CheckResponseExceptions(await requestMessage.GetRequestInfo(), response, json);

        var jsonEl = _jsonSerializer.Parse(json);
        var accessToken = await ReadAccessTokenAsync(jsonEl);

        OnlineAccessInfo? onlineAccessInfo = null;

        if (jsonEl.TryGetProperty(AssociatedUserPropertyName, out var user) && user.ValueType != JsonValueType.Null)
        {
            if (user.ValueType != JsonValueType.Object)
                throw new ShopifyJsonParseException(
                    $"The JSON response from Shopify does not contain a valid '{AssociatedUserPropertyName}' property. The property type was {user.ValueType}, which is invalid.",
                    AssociatedUserPropertyName
                );

            var expiresInElem = GetRequiredProperty(jsonEl, ExpiresInPropertyName, JsonValueType.Number);
            var expiresInSec = await _jsonSerializer.DeserializeAsync<int>(expiresInElem);
            var userScopes = await ReadScopesToArrayAsync(jsonEl, AssociatedUserScopePropertyName);
            var associatedUser = await _jsonSerializer.DeserializeAsync<AssociatedUser>(user);

            onlineAccessInfo = new OnlineAccessInfo
            {
                ExpiresIn = TimeSpan.FromSeconds(expiresInSec),
                AssociatedUserScopes = userScopes,
                AssociatedUser = associatedUser!
            };
        }

        var scopes = await ReadScopesToArrayAsync(jsonEl, ScopePropertyName);
        return new AuthorizationResult(accessToken, scopes)
        {
            OnlineAccess = onlineAccessInfo
        };
    }

    private static IJsonElement GetRequiredProperty(IJsonElement json, string propertyName, JsonValueType expectedType)
    {
        if (!json.TryGetProperty(propertyName, out var property) || property.ValueType == JsonValueType.Null)
            throw new ShopifyJsonParseException(
                $"The JSON response from Shopify does not contain a valid '{propertyName}' property. The property was null or missing.",
                propertyName
            );
        if (property.ValueType != expectedType)
            throw new ShopifyJsonParseException(
                $"The JSON response from Shopify does not contain a valid '{propertyName}' property. The property type was {property.ValueType}, which is invalid.",
                propertyName
            );
        return property;
    }

    private async ValueTask<string> ReadAccessTokenAsync(IJsonElement json)
    {
        var accessTokenStr = await _jsonSerializer.DeserializeAsync<string>(GetRequiredProperty(json, AccessTokenPropertyName, JsonValueType.String));

        if (accessTokenStr is null || string.IsNullOrWhiteSpace(accessTokenStr))
            throw new ShopifyJsonParseException(
                $"The JSON response from Shopify does not contain a valid '{AccessTokenPropertyName}' property. The property was null or empty.",
                AccessTokenPropertyName
            );

        return accessTokenStr;
    }

    private async ValueTask<string[]> ReadScopesToArrayAsync(IJsonElement json, string propertyName)
    {
        if (!json.TryGetProperty(propertyName, out var scopesElement) || scopesElement.ValueType == JsonValueType.Null)
            return [];

        if (scopesElement.ValueType != JsonValueType.String)
            throw new ShopifyJsonParseException(
                $"The JSON response from Shopify does not contain a valid '{propertyName}' property. The property type was {scopesElement.ValueType}, which is invalid.",
                propertyName
            );

        var value = await _jsonSerializer.DeserializeAsync<string>(scopesElement);
        return value?
            .Trim()
            .Split(',')
            .Select(s => s.Trim())
            .Where(s => s.Length > 0)
            .ToArray() ?? [];
    }

    /// <inheritdoc />
    public Task<AuthorizationResult> RefreshAccessTokenAsync(RefreshAccessTokenOptions options) =>
        RefreshAccessTokenAsync(
            options.ShopDomain,
            options.ClientId,
            options.ClientSecret,
            options.RefreshToken,
            options.ExistingStoreAccessToken
        );

    /// <inheritdoc />
    public async Task<AuthorizationResult> RefreshAccessTokenAsync(
        string shopDomain,
        string clientId,
        string clientSecret,
        string refreshToken,
        string existingStoreAccessToken
    )
    {
        var ub = new UriBuilder(_domainUtility.BuildShopDomainUri(shopDomain))
        {
            Path = "admin/oauth/access_token"
        };
        using var content = new JsonContent(new
        {
            client_id = clientId,
            client_secret = clientSecret,
            refresh_token = refreshToken,
            access_token = existingStoreAccessToken
        });
        using var request = new CloneableRequestMessage(ub.Uri, HttpMethod.Post, content);

        return await SendRequestAndParseAuthorizationResultAsync(request);
    }
}
